Skip to content

Fix home-page flicker AND post-login non-interactive page (#725)#1318

Merged
milanmajchrak merged 1 commit into
customer/vsb-tuofrom
fix/flickering-and-slow-login
Jun 22, 2026
Merged

Fix home-page flicker AND post-login non-interactive page (#725)#1318
milanmajchrak merged 1 commit into
customer/vsb-tuofrom
fix/flickering-and-slow-login

Conversation

@milanmajchrak

@milanmajchrak milanmajchrak commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

Problem

Issue dspace-customers#725 and the tug-of-war between PR #1288 and PR #1317:

Root cause — why #1288 and #1317 conflict

This FE is Angular 15 + ngUniversal with no hydration (provideClientHydration is Angular 16+), so every browser load tears down the SSR DOM and re-renders from scratch. PR #1288 masks that rebuild with a snapshot overlay (#__dspace_ssr_overlay, pointer-events:none) over a hidden <ds-app>, removed via window.__dspaceRemoveSsrOverlay(). Both PRs edit the same decision: when to call that removal, and each picks a signal at the opposite end of one tradeoff:

PR Removal trigger Result
#1288 ApplicationRef.isStable Content always painted ⇒ no flicker. But isStable is held hostage by any ongoing zone async — after an admin login (authz/widgets, polling, AAI scripts) it fires many seconds late or hits the 15 s fallback ⇒ page masked & non-interactive (#725).
#1317 !isAuthenticationBlocking && !isThemeLoading + 1×requestAnimationFrame Fires promptly ⇒ fixes #725. But that gate only un-hides <router-outlet>; Angular hasn't rendered the routed page yet and one rAF runs before paint ⇒ snapshot dropped over an empty <ds-app>flicker returns.

Neither signal means "the routed content for this page is committed to the DOM and painted, and the app is interactive." isStable over-waits (couples removal to unrelated background work); the auth/theme gate under-waits (decoupled from actual content paint). So fixing one re-breaks the other.

The fix (src/app/app.component.ts)

Keep #1317's decoupling from isStable (so background async can never delay us), but after the gate opens, wait across animation frames until the real <ds-app> is actually laid out (height ≥ 200px and its #main-content host present — i.e. no longer the empty shell the overlay script left) before dropping the snapshot, one frame later. Capped at MAX_FRAMES (~3 s) so nothing can hold the overlay open; the 15 s hard fallback in index.html stays as the catastrophic-error net.

This is deterministic, not a favourable race: removal happens only after contentPainted() is true, so the real <ds-app> is never empty when the snapshot drops.

Verification (DSpace 7.6.5 backend, Playwright + Chromium, CPU throttled 4×, hard reload / ctrl+shift+R)

Measured on one performance.now() timeline. overlayRemoved ≈ time-to-interactive (overlay is pointer-events:none over a hidden <ds-app>). ds-app height @ removal = 0 ⇒ blank flash.

Scenario Code TTI content painted ds-app height @ removal Verdict
admin reload #1288 15963 ms 2947 ms 5281 px SLOW: ~13 s masked (#725)
admin reload #1317 3797 ms 4014 ms 0 px FLICKER: 217 ms blank
admin reload this fix 3064–3421 ms 3068 ms 5281 px NO-FLICKER + interactive ~3.4 s
anon reload this fix 2911 ms 2357 ms 1461 px NO-FLICKER

Both after-fix runs: 0 CORS errors and 0 SSR/hydration/NG0/ExpressionChanged console errors. Admin run repeated ×3 — all NO-FLICKER (height 5281 px, gap ≤ 0).

"No flicker", measurably: at the instant the overlay is removed the real <ds-app> is not empty (height ≥ 200px with #main-content), so the blank window max(0, contentReady − overlayRemoved) is ≤ 0 ms.

Verification videos

Hosted on the flicker-fix-evidence branch (kept off this PR's diff). Click to play in GitHub's in-browser player, or download:

Before

After (this fix)

Notes

  • Existing AppComponent specs (removeSsrOverlayWhenContentVisible) are unchanged and remain valid (synchronous-rAF shim + frame cap ⇒ removal still fires once). The new content-paint wait is verified via the Playwright runs above. The full karma suite was not run locally; CI covers it.
  • The viewevents / google.analytics console entries seen during measurement are the throwaway test backend lacking statistics/analytics config — not SSR/hydration errors and not present on VSB.

Refs: dspace-customers#725, PR #1288, PR #1317

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f7bda491-859c-4b9c-acc1-1db00748d4cb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

… on isStable or bare auth gate)

The SSR anti-flicker overlay (PR #1288) is removed by AppComponent. Two prior
approaches each fixed one symptom and reintroduced the other:

- #1288 waited for ApplicationRef.isStable: no flicker, but admin sessions keep
  the zone busy (authz/widgets, polling, AAI scripts) so isStable fires many
  seconds late (or hits the 15s fallback), leaving the live, already-rendered page
  masked & non-interactive (dspace-customers#725).
- #1317 switched to the loader-swap gate (!isAuthenticationBlocking &&
  !isThemeLoading) + a single requestAnimationFrame: fast/interactive, but the
  gate only un-hides <router-outlet> and one rAF runs before paint, so the
  snapshot is dropped over an empty <ds-app> -> the flicker returned.

Neither signal means "the routed content is painted AND the app is interactive":
isStable over-waits (couples removal to unrelated background async); the auth/theme
gate under-waits (decoupled from actual content paint).

This keeps #1317's decoupling from isStable but, after the gate opens, waits across
animation frames until the real <ds-app> is actually laid out (height >= 200px AND
its #main-content host present) before removing the overlay, capped at ~3s
(MAX_FRAMES) so background async can never hold it open. The 15s fallback in
index.html stays as the catastrophic-error net.

Verified (DSpace 7.6.5 backend, CPU 4x, hard reload, admin session):
- #1288: TTI 15963ms (page masked ~13s)  #1317: ds-app height 0 at removal (217ms flash)
- fix: TTI ~3.1-3.4s, ds-app height 5281px at removal, gap <= 0 (no flash), 3x deterministic;
  anon reload also no-flicker; 0 CORS and 0 SSR/hydration/NG0 console errors.
Verification videos are linked in the PR description.

Refs: dspace-customers#725, PR #1288, PR #1317

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@milanmajchrak milanmajchrak force-pushed the fix/flickering-and-slow-login branch from 1a23f47 to 43fb507 Compare June 22, 2026 07:19
@milanmajchrak milanmajchrak merged commit dce52f0 into customer/vsb-tuo Jun 22, 2026
6 checks passed
milanmajchrak added a commit that referenced this pull request Jun 23, 2026
…er WebDriver (close #725)

Addresses an expert review of the DOM-settle change: DOM-settle alone shortened but did not
eliminate issue #725, because the masking window still left the page non-interactive (the opaque
overlay sat over a visibility:hidden <ds-app>, so clicks landed on nothing) and still produced
duplicate DOM that breaks strict-mode E2E locators.

Changes:
- index.html: drop the `ds-app[data-dspace-ssr-hidden]{visibility:hidden}` rule and give the overlay
  min-height:100vh. The overlay is already opaque + pointer-events:none, so it still masks the CSR
  rebuild visually, but clicks now pass THROUGH it to the real, still-visible <ds-app> underneath.
  The page is therefore interactive the entire time it is masked -> #725 ("looks rendered but
  nothing is clickable") cannot recur, even if removal rides the 10s cap. The snapshot is marked
  aria-hidden so AT (and the duplicate-node concern) target the live app, not the snapshot.
- index.html: also bypass the overlay for WebDriver runners (navigator.webdriver), mirroring the
  existing Cypress guard, so Playwright/Selenium see no overlay and no duplicate DOM (fixes the
  #725 strict-mode locator failure).
- app.component.ts: exclude the admin-sidebar subtree from the DOM-settle MutationObserver (its long
  :enter/:leave animations would keep re-arming the quiet window and push admin logins toward the
  cap); add OnDestroy + takeUntil + observer/timer teardown.
- Fix stale `ApplicationRef.isStable` / `removeSsrOverlayWhenStable` comments (index.html, typings.d.ts).

Verified against the live build (Playwright, CPU/network throttled):
- WebDriver run: overlay absent (no duplicate DOM).
- webdriver spoofed false (real-user path): a navbar click WHILE the snapshot is still shown reaches
  the live <ds-app> (interactive under mask).
- anon reveal settled, CLS after reveal = 0; admin reveal settled (reason "settled", not cap),
  CLS after reveal = 0.

Refs: dspace-customers#725, PR #1288, PR #1317, PR #1318, PR #1321

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
milanmajchrak added a commit that referenced this pull request Jun 23, 2026
…moved only after the routed DOM settles (#1321)

* fix(ssr-overlay): remove overlay only after the routed page DOM settles (real home-page flicker)

Follow-up to #1318. On a real instance (VSB / dev-6.pc), an incognito Ctrl+Shift+R
still flickered: the deployed code dropped the SSR snapshot ~3.2s in, while the home
page was only half-rendered (search box, community list and several navbar items not
yet present, content showing "Recent Submissions") and then everything popped into
place ~600ms later. Captured frame-by-frame against the live instance.

Root cause: the home page renders piecewise (each section fetches its own data), so the
previous "<ds-app> has #main-content and height>=200" heuristic was satisfied while the
page was still building -> snapshot removed too early -> visible flicker.

Fix: after the auth/theme gate opens, keep the snapshot until the real <ds-app> subtree
has SETTLED -- no element added/removed for SETTLE_QUIET_MS (600ms) -- via a
MutationObserver, requiring minimum content first and capped at SETTLE_MAX_MS (10s). This
stays decoupled from ApplicationRef.isStable (DOM-settle ignores non-rendering background
async), so it does not bring back the post-login non-interactive page (#725): admin
reveals in ~5s, not ~15s.

Validated against the live dev-6.pc instance by intercepting the overlay-removal and
driving it with this condition:
- anon reload : drops @ ~4.8s, page COMPLETE (search + community list + full navbar)
- admin reload: drops @ ~5.0s (reason "settled", not the cap), page complete
vs the deployed code dropping @ ~3.2-3.6s on a half-built page.

Refs: dspace-customers#725, PR #1288, PR #1317, PR #1318

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(ssr-overlay): make the masked page interactive + skip overlay under WebDriver (close #725)

Addresses an expert review of the DOM-settle change: DOM-settle alone shortened but did not
eliminate issue #725, because the masking window still left the page non-interactive (the opaque
overlay sat over a visibility:hidden <ds-app>, so clicks landed on nothing) and still produced
duplicate DOM that breaks strict-mode E2E locators.

Changes:
- index.html: drop the `ds-app[data-dspace-ssr-hidden]{visibility:hidden}` rule and give the overlay
  min-height:100vh. The overlay is already opaque + pointer-events:none, so it still masks the CSR
  rebuild visually, but clicks now pass THROUGH it to the real, still-visible <ds-app> underneath.
  The page is therefore interactive the entire time it is masked -> #725 ("looks rendered but
  nothing is clickable") cannot recur, even if removal rides the 10s cap. The snapshot is marked
  aria-hidden so AT (and the duplicate-node concern) target the live app, not the snapshot.
- index.html: also bypass the overlay for WebDriver runners (navigator.webdriver), mirroring the
  existing Cypress guard, so Playwright/Selenium see no overlay and no duplicate DOM (fixes the
  #725 strict-mode locator failure).
- app.component.ts: exclude the admin-sidebar subtree from the DOM-settle MutationObserver (its long
  :enter/:leave animations would keep re-arming the quiet window and push admin logins toward the
  cap); add OnDestroy + takeUntil + observer/timer teardown.
- Fix stale `ApplicationRef.isStable` / `removeSsrOverlayWhenStable` comments (index.html, typings.d.ts).

Verified against the live build (Playwright, CPU/network throttled):
- WebDriver run: overlay absent (no duplicate DOM).
- webdriver spoofed false (real-user path): a navbar click WHILE the snapshot is still shown reaches
  the live <ds-app> (interactive under mask).
- anon reveal settled, CLS after reveal = 0; admin reveal settled (reason "settled", not cap),
  CLS after reveal = 0.

Refs: dspace-customers#725, PR #1288, PR #1317, PR #1318, PR #1321

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(ssr-overlay): declarative DOM-settle, drop fragile admin-sidebar hack (no behaviour change)

The overlay-removal logic was a hard-to-read imperative blob (mutable done/quietTimer/capTimer flags,
manual arm/re-arm, manual MutationObserver/teardown bookkeeping) plus a brittle string-selector hack
(`inAdminSidebar` -> `closest('ds-themed-admin-sidebar, ds-admin-sidebar')`).

Rewritten as small, named, single-responsibility pieces:
- removeSsrOverlayWhenContentVisible(): guard + runOutsideAngular + subscribe(takeUntil(destroyed$)).
- routedPageReadyToReveal$(): loader gate (take 1) -> switchMap(dsAppDomSettled$).
- dsAppDomSettled$(): MutationObserver wrapped as an Observable; settle = startWith + debounceTime
  (the quiet window) + filter(real content); race() with timer() for the cap; take(1).
- dsAppHasRenderedContent(), isElementChildListChange(), runAfterNextFrame(): tiny pure helpers.

RxJS now owns debounce, the cap, and teardown (the Observable disconnects the observer on unsubscribe,
takeUntil(destroyed$) ends everything on destroy), so the mutable flags, manual timers and the separate
cancelOverlaySettle field are gone (net -45 lines).

Dropped the admin-sidebar exclusion entirely: it was a fragile, theme-coupled selector guarding a
problem that interactive-under-mask already makes harmless (riding the cap is fine when the page is
clickable throughout) and that does not occur in practice (admin still settles via "settled", not the
cap). Tuning constants moved to named readonly fields.

Behaviour re-verified on the live build (Playwright, throttled): interactive-under-mask still works;
anon + admin both reveal with reason "settled" (not cap) and CLS after reveal = 0.

Refs: dspace-customers#725, PR #1321

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
jr-rk added a commit that referenced this pull request Jun 30, 2026
Supersedes the #1317 content-visible trigger backported earlier. That gate
(!isAuthenticationBlocking && !isThemeLoading) still revealed a half-built page
on hard reload, so VSB-TUO's #1318/#1321 keep the snapshot until the routed
<ds-app> DOM has SETTLED (MutationObserver + quiet window, with a content
height / #main-content check and a 10s cap). The overlay is now a purely
visual mask, so the live app stays interactive underneath while it rebuilds
(closes dspace-customers#725 - "looks rendered but not clickable").

index.html, app.component.ts, spec and typings are synced to VSB-TUO's final
version; the VSB-only ngAfterViewInit delay(0) is omitted (these instances
don't carry it).

Ref: #1318, #1321

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jr-rk added a commit that referenced this pull request Jun 30, 2026
Supersedes the #1317 content-visible trigger backported earlier. That gate
(!isAuthenticationBlocking && !isThemeLoading) still revealed a half-built page
on hard reload, so VSB-TUO's #1318/#1321 keep the snapshot until the routed
<ds-app> DOM has SETTLED (MutationObserver + quiet window, with a content
height / #main-content check and a 10s cap). The overlay is now a purely
visual mask, so the live app stays interactive underneath while it rebuilds
(closes dspace-customers#725 - "looks rendered but not clickable").

index.html, app.component.ts, spec and typings are synced to VSB-TUO's final
version; the VSB-only ngAfterViewInit delay(0) is omitted (these instances
don't carry it).

Ref: #1318, #1321

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jr-rk added a commit that referenced this pull request Jun 30, 2026
Supersedes the #1317 content-visible trigger backported earlier. That gate
(!isAuthenticationBlocking && !isThemeLoading) still revealed a half-built page
on hard reload, so VSB-TUO's #1318/#1321 keep the snapshot until the routed
<ds-app> DOM has SETTLED (MutationObserver + quiet window, with a content
height / #main-content check and a 10s cap). The overlay is now a purely
visual mask, so the live app stays interactive underneath while it rebuilds
(closes dspace-customers#725 - "looks rendered but not clickable").

index.html, app.component.ts, spec and typings are synced to VSB-TUO's final
version; the VSB-only ngAfterViewInit delay(0) is omitted (these instances
don't carry it).

Ref: #1318, #1321

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jr-rk added a commit that referenced this pull request Jun 30, 2026
Supersedes the #1317 content-visible trigger backported earlier. That gate
(!isAuthenticationBlocking && !isThemeLoading) still revealed a half-built page
on hard reload, so VSB-TUO's #1318/#1321 keep the snapshot until the routed
<ds-app> DOM has SETTLED (MutationObserver + quiet window, with a content
height / #main-content check and a 10s cap). The overlay is now a purely
visual mask, so the live app stays interactive underneath while it rebuilds
(closes dspace-customers#725 - "looks rendered but not clickable").

index.html, app.component.ts, spec and typings are synced to VSB-TUO's final
version; the VSB-only ngAfterViewInit delay(0) is omitted (these instances
don't carry it).

Ref: #1318, #1321

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jr-rk added a commit that referenced this pull request Jun 30, 2026
Rebuilds this follow-up on the current customer/vsb-tuo base (which now carries
the final overlay mechanism #1318/#1321 and #1333). Drops the duplicate dspace
eager-theme import (imported once as DSpaceEagerThemeModule and again, unaliased,
as EagerThemeModule) so this file matches the canonical form on TUL/SAV/ZCU-DATA/
ZCU-PUB: dspace eager theme once + custom eager theme once.

The earlier blank-page guard from this branch is intentionally dropped: #1321
removed the ds-app visibility:hidden rule (the overlay is now a purely visual
mask), so the early-return can no longer hide the app, and the other customers
took VSB-TUO's #1321 index.html verbatim. Keeping a guard only here would
re-diverge index.html.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jr-rk added a commit that referenced this pull request Jul 1, 2026
…1318, #1321)

The original overlay removal waited for ApplicationRef.isStable, which is held
hostage by post-login/admin zone activity (page looks rendered but is not
interactive - dspace-customers#725). Replace it with the final mechanism used
across the customer backports: keep the SSR snapshot until the routed <ds-app>
DOM has settled (MutationObserver + quiet window, content-height / #main-content
check, 10s cap), and make the overlay a purely visual mask so the live app stays
interactive underneath. index.html / app.component.ts / spec / typings are synced
to the final version; the existing ngAfterViewInit delay(0) is preserved.

Keeping the trunk on the final mechanism means new customer branches cut from
dtq-dev start correct instead of re-inheriting the old isStable approach.

Ref: #1318, #1321

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jr-rk added a commit that referenced this pull request Jul 1, 2026
Mendelu was the last instance still on the original ApplicationRef.isStable
trigger. isStable is held hostage by post-login/admin zone activity, so the
hydration-safe clone overlay lingered over an app that had long finished (the
same class of problem as dspace-customers#725 - visually here, since the clone
lets clicks through, but the frozen frame still sat there for up to 15s).

Port the final trigger used across the other customers: drop the clone once the
auth/theme loader gate opens AND the live <ds-app> DOM has settled (MutationObserver
+ quiet window, content-height / #main-content check, 10s cap), decoupled from
isStable. The Angular-18 hydration-safe CLONE index.html is unchanged (comments
only); the DOM-settle trigger lives in AppComponent. Spec, theme-service mock and
an OnDestroy teardown updated to match.

NOTE: hand-port to the Angular-18 stack - CI validates compile + unit tests but
NOT the real hydration + clone + DOM-settle timing; needs a visual check on a
running Mendelu instance (authenticated hard reload) before merge.

Ref: #1318, #1321

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
milanmajchrak pushed a commit that referenced this pull request Jul 2, 2026
* Backport of Fix home-page SSR->CSR flicker

* Fix: always unhide app when removing SSR anti-flicker overlay

The overlay remover bailed out via `if (!el) return;` before unhiding
<ds-app>, so if the overlay node went missing (browser extension, race,
external script) the app stayed visibility:hidden forever -> blank page,
plus the kept SSR styles leaked. Unhide the app and clean up the kept
styles unconditionally, before checking for the overlay node.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Chore: drop accidentally committed build/spec logs

_build.log and _spec.log are local deploy-tooling output that should
never have been tracked. Remove them and gitignore /_*.log.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Test: isolate isStable override and cover the no-rAF overlay path

Make the ApplicationRef.isStable override in the removeSsrOverlayWhenStable
suite configurable and restore the original descriptor in afterEach, so the
patched observable can't leak onto the shared TestBed instance. Add a test
for the requestAnimationFrame-absent fallback branch of the remover.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Refactor: align eager-themes.module.ts with the other backport instances

Bind the custom eager theme to the CustomEagerThemeModule alias used by the
other customer backports so this file is byte-identical across instances.
The same ./custom/eager-theme.module is still imported eagerly - no runtime,
build, or bundle-size change; the custom theme stays eager (which also keeps
the untyped-item theming working, ref DSpace#1897).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Build: raise initial bundle budget to 5.5mb/6mb to match the other backports

Aligns ZCU-PUB's initial budget with the value the root fix (#1287) and the
other customer backports use, so the budget block is identical across
instances. The custom theme is already eager here, so this only widens the
headroom; the build already passes under the previous 5mb error ceiling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Refactor: remove SSR overlay on content-visible instead of isStable

Propagates VSB-TUO's fix #1317 to this instance. The overlay was removed when
ApplicationRef.isStable settled, but isStable can be delayed for seconds by
post-login admin zone activity (auth work, background polling, third-party
scripts) - during which the live app stays hidden under the SSR mask and the
page renders but is non-interactive (dataquest-dev/dspace-customers#725).

Switch removal to the same condition root.component.html uses to show real
content: !isAuthenticationBlocking && !isThemeLoading. Drop the now-unused
ApplicationRef injection and the 50ms pad; keep the 15s hard fallback as a
catastrophic safety net. Tests and the theme-service mock updated to match.

Ref: #1317

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Backport final SSR-overlay mechanism from VSB-TUO (#1318, #1321)

Supersedes the #1317 content-visible trigger backported earlier. That gate
(!isAuthenticationBlocking && !isThemeLoading) still revealed a half-built page
on hard reload, so VSB-TUO's #1318/#1321 keep the snapshot until the routed
<ds-app> DOM has SETTLED (MutationObserver + quiet window, with a content
height / #main-content check and a 10s cap). The overlay is now a purely
visual mask, so the live app stays interactive underneath while it rebuilds
(closes dspace-customers#725 - "looks rendered but not clickable").

index.html, app.component.ts, spec and typings are synced to VSB-TUO's final
version; the VSB-only ngAfterViewInit delay(0) is omitted (these instances
don't carry it).

Ref: #1318, #1321

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
milanmajchrak pushed a commit that referenced this pull request Jul 2, 2026
* Backport of Fix home-page SSR->CSR flicker

* Fix: always unhide app when removing SSR anti-flicker overlay

The overlay remover bailed out via `if (!el) return;` before unhiding
<ds-app>, so if the overlay node went missing (browser extension, race,
external script) the app stayed visibility:hidden forever -> blank page,
plus the kept SSR styles leaked. Unhide the app and clean up the kept
styles unconditionally, before checking for the overlay node.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Chore: drop accidentally committed build/spec logs

_build.log and _spec.log are local deploy-tooling output that should
never have been tracked. Remove them and gitignore /_*.log.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Test: isolate isStable override and cover the no-rAF overlay path

Make the ApplicationRef.isStable override in the removeSsrOverlayWhenStable
suite configurable and restore the original descriptor in afterEach, so the
patched observable can't leak onto the shared TestBed instance. Add a test
for the requestAnimationFrame-absent fallback branch of the remover.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Refactor: remove SSR overlay on content-visible instead of isStable

Propagates VSB-TUO's fix #1317 to this instance. The overlay was removed when
ApplicationRef.isStable settled, but isStable can be delayed for seconds by
post-login admin zone activity (auth work, background polling, third-party
scripts) - during which the live app stays hidden under the SSR mask and the
page renders but is non-interactive (dataquest-dev/dspace-customers#725).

Switch removal to the same condition root.component.html uses to show real
content: !isAuthenticationBlocking && !isThemeLoading. Drop the now-unused
ApplicationRef injection and the 50ms pad; keep the 15s hard fallback as a
catastrophic safety net. Tests and the theme-service mock updated to match.

Ref: #1317

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Backport final SSR-overlay mechanism from VSB-TUO (#1318, #1321)

Supersedes the #1317 content-visible trigger backported earlier. That gate
(!isAuthenticationBlocking && !isThemeLoading) still revealed a half-built page
on hard reload, so VSB-TUO's #1318/#1321 keep the snapshot until the routed
<ds-app> DOM has SETTLED (MutationObserver + quiet window, with a content
height / #main-content check and a 10s cap). The overlay is now a purely
visual mask, so the live app stays interactive underneath while it rebuilds
(closes dspace-customers#725 - "looks rendered but not clickable").

index.html, app.component.ts, spec and typings are synced to VSB-TUO's final
version; the VSB-only ngAfterViewInit delay(0) is omitted (these instances
don't carry it).

Ref: #1318, #1321

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
milanmajchrak pushed a commit that referenced this pull request Jul 2, 2026
* Backport of Fix home-page SSR->CSR flicker

* Fix: always unhide app when removing SSR anti-flicker overlay

The overlay remover bailed out via `if (!el) return;` before unhiding
<ds-app>, so if the overlay node went missing (browser extension, race,
external script) the app stayed visibility:hidden forever -> blank page,
plus the kept SSR styles leaked. Unhide the app and clean up the kept
styles unconditionally, before checking for the overlay node.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Chore: drop accidentally committed build/install/spec logs

_build.log, _install.log and _spec.log are local deploy-tooling output
that should never have been tracked. Remove them and gitignore /_*.log.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Test: isolate isStable override and cover the no-rAF overlay path

Make the ApplicationRef.isStable override in the removeSsrOverlayWhenStable
suite configurable and restore the original descriptor in afterEach, so the
patched observable can't leak onto the shared TestBed instance. Add a test
for the requestAnimationFrame-absent fallback branch of the remover.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Refactor: remove SSR overlay on content-visible instead of isStable

Propagates VSB-TUO's fix #1317 to this instance. The overlay was removed when
ApplicationRef.isStable settled, but isStable can be delayed for seconds by
post-login admin zone activity (auth work, background polling, third-party
scripts) - during which the live app stays hidden under the SSR mask and the
page renders but is non-interactive (dataquest-dev/dspace-customers#725).

Switch removal to the same condition root.component.html uses to show real
content: !isAuthenticationBlocking && !isThemeLoading. Drop the now-unused
ApplicationRef injection and the 50ms pad; keep the 15s hard fallback as a
catastrophic safety net. Tests and the theme-service mock updated to match.

Ref: #1317

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Backport final SSR-overlay mechanism from VSB-TUO (#1318, #1321)

Supersedes the #1317 content-visible trigger backported earlier. That gate
(!isAuthenticationBlocking && !isThemeLoading) still revealed a half-built page
on hard reload, so VSB-TUO's #1318/#1321 keep the snapshot until the routed
<ds-app> DOM has SETTLED (MutationObserver + quiet window, with a content
height / #main-content check and a 10s cap). The overlay is now a purely
visual mask, so the live app stays interactive underneath while it rebuilds
(closes dspace-customers#725 - "looks rendered but not clickable").

index.html, app.component.ts, spec and typings are synced to VSB-TUO's final
version; the VSB-only ngAfterViewInit delay(0) is omitted (these instances
don't carry it).

Ref: #1318, #1321

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
milanmajchrak pushed a commit that referenced this pull request Jul 2, 2026
* AI backport of Fix home-page SSR->CSR flicker

* Fix: always unhide app when removing SSR anti-flicker overlay

The overlay remover bailed out via `if (!el) return;` before unhiding
<ds-app>, so if the overlay node went missing (browser extension, race,
external script) the app stayed visibility:hidden forever -> blank page,
plus the kept SSR styles leaked. Unhide the app and clean up the kept
styles unconditionally, before checking for the overlay node.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Test: isolate isStable override and cover the no-rAF overlay path

Make the ApplicationRef.isStable override in the removeSsrOverlayWhenStable
suite configurable and restore the original descriptor in afterEach, so the
patched observable can't leak onto the shared TestBed instance. Add a test
for the requestAnimationFrame-absent fallback branch of the remover.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Refactor: remove SSR overlay on content-visible instead of isStable

Propagates VSB-TUO's fix #1317 to this instance. The overlay was removed when
ApplicationRef.isStable settled, but isStable can be delayed for seconds by
post-login admin zone activity (auth work, background polling, third-party
scripts) - during which the live app stays hidden under the SSR mask and the
page renders but is non-interactive (dataquest-dev/dspace-customers#725).

Switch removal to the same condition root.component.html uses to show real
content: !isAuthenticationBlocking && !isThemeLoading. Drop the now-unused
ApplicationRef injection and the 50ms pad; keep the 15s hard fallback as a
catastrophic safety net. Tests and the theme-service mock updated to match.

Ref: #1317

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Backport final SSR-overlay mechanism from VSB-TUO (#1318, #1321)

Supersedes the #1317 content-visible trigger backported earlier. That gate
(!isAuthenticationBlocking && !isThemeLoading) still revealed a half-built page
on hard reload, so VSB-TUO's #1318/#1321 keep the snapshot until the routed
<ds-app> DOM has SETTLED (MutationObserver + quiet window, with a content
height / #main-content check and a 10s cap). The overlay is now a purely
visual mask, so the live app stays interactive underneath while it rebuilds
(closes dspace-customers#725 - "looks rendered but not clickable").

index.html, app.component.ts, spec and typings are synced to VSB-TUO's final
version; the VSB-only ngAfterViewInit delay(0) is omitted (these instances
don't carry it).

Ref: #1318, #1321

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
milanmajchrak pushed a commit that referenced this pull request Jul 2, 2026
* Backport of Fix home-page SSR->CSR flicker

* Test: isolate isStable override and cover the no-rAF overlay path

Make the ApplicationRef.isStable override in the removeSsrOverlayWhenStable
suite configurable and restore the original descriptor in afterEach, so the
patched observable can't leak onto the shared TestBed instance. Add a test
for the requestAnimationFrame-absent fallback branch of the remover.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Fix: mark the SSR clone overlay inert so it can't trap keyboard focus

The overlay holds a deep clone of the SSR DOM purely as a freeze-frame. It
was aria-hidden and pointer-events:none, but Tab focus could still land on
the dead cloned controls. Add the inert attribute to take it out of the tab
order while it exists.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Backport final DOM-settle overlay mechanism to Mendelu (#1318, #1321)

Mendelu was the last instance still on the original ApplicationRef.isStable
trigger. isStable is held hostage by post-login/admin zone activity, so the
hydration-safe clone overlay lingered over an app that had long finished (the
same class of problem as dspace-customers#725 - visually here, since the clone
lets clicks through, but the frozen frame still sat there for up to 15s).

Port the final trigger used across the other customers: drop the clone once the
auth/theme loader gate opens AND the live <ds-app> DOM has settled (MutationObserver
+ quiet window, content-height / #main-content check, 10s cap), decoupled from
isStable. The Angular-18 hydration-safe CLONE index.html is unchanged (comments
only); the DOM-settle trigger lives in AppComponent. Spec, theme-service mock and
an OnDestroy teardown updated to match.

NOTE: hand-port to the Angular-18 stack - CI validates compile + unit tests but
NOT the real hydration + clone + DOM-settle timing; needs a visual check on a
running Mendelu instance (authenticated hard reload) before merge.

Ref: #1318, #1321

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: Reformatted the import into multiline style in app.component.spec.ts:20

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
milanmajchrak pushed a commit that referenced this pull request Jul 2, 2026
…r backports (#1320)

Rebuilds this follow-up on the current customer/vsb-tuo base (which now carries
the final overlay mechanism #1318/#1321 and #1333). Drops the duplicate dspace
eager-theme import (imported once as DSpaceEagerThemeModule and again, unaliased,
as EagerThemeModule) so this file matches the canonical form on TUL/SAV/ZCU-DATA/
ZCU-PUB: dspace eager theme once + custom eager theme once.

The earlier blank-page guard from this branch is intentionally dropped: #1321
removed the ds-app visibility:hidden rule (the overlay is now a purely visual
mask), so the early-return can no longer hide the app, and the other customers
took VSB-TUO's #1321 index.html verbatim. Keeping a guard only here would
re-diverge index.html.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
milanmajchrak pushed a commit that referenced this pull request Jul 2, 2026
* Fix home-page SSR->CSR flicker

Angular 15 has no provideClientHydration; on every browser load Angular
tears down the entire SSR DOM and rebuilds the component tree from scratch.
Measured CLS = 0.89 at t=1.76s on /home (PerformanceObserver on dev-5).
The visible flicker is that ~600ms rebuild window between SSR view and
populated CSR view.

Two compounding causes, addressed in this PR:

1. CustomEagerThemeModule was commented out in src/themes/eager-themes.module.ts,
   so every custom-themed wrapper (footer, header, root, ...) was lazy-loaded via
   webpack code-splitting on the browser, stretching the gap. Re-enable it
   (the existing custom/eager-theme.module.ts already declares the right set).
   Bumps initial bundle by ~256KB; angular.json budget raised from 5MB to 8MB
   to accommodate.

2. The bigger cause - no hydration - is masked by an inline pre-bootstrap script
   in src/index.html that:
     - Captures all <style ng-transition="dspace-angular"> blocks into
       <style data-dspace-ssr-keep> tags Angular won't strip (Angular removes
       the originals on bootstrap, which is why a naive overlay renders
       unstyled).
     - Moves (not clones) the SSR-rendered <ds-app> children into an
       absolute-positioned overlay so they keep every live DOM/style detail.
     - Hides the now-empty <ds-app> via a data-attribute and CSS rule.
     - Exposes window.__dspaceRemoveSsrOverlay() for AppComponent to call
       once ApplicationRef.isStable fires (with one rAF + 50ms pad).
     - 15s safety fallback in case isStable never fires.

Bots and no-JS users still get the original SSR <ds-app> (the overlay is
JS-added). Real users see continuous SSR-rendered content while CSR rebuilds
invisibly underneath, then a 150ms fade reveals the CSR DOM in its final
data-loaded state.

Verified locally via Service Worker that suppresses the removal: overlay's
header height is 80px (proper styling preserved) versus 698px (the unstyled
fallback before this fix's style-preservation step).

Includes a small Windows cmd deploy helper at scripts/dspace-deploy.bat and
matching skill doc at .claude/skills/dspace-deploy/SKILL.md - multi-instance
safe local dev stack via the existing docker compose files.

* Lint: curly braces around early-return if

* Address Copilot review

- angular.json: tighten budget back to 5.5MB warn / 6MB error (was 8MB)
- index.html: re-entrancy guard on __dspaceRemoveSsrOverlay (null the
  pointer up-front so the isStable + 15s safety fallback can't double-fade)
- index.html: drop aria-hidden from overlay so screen-reader users get the
  SSR snapshot during boot (ds-app underneath has visibility:hidden which
  already excludes it from a11y tree)
- index.html: console.warn on the overlay-script catch so a silently broken
  flicker fix is at least diagnosable in DevTools
- typings.d.ts: typed Window.__dspaceRemoveSsrOverlay augmentation; drop
  the `as any` cast in AppComponent.removeSsrOverlayWhenStable
- app.component.spec.ts: cover removeSsrOverlayWhenStable (calls the
  global once on isStable=true; no-op when global absent)
- Drop scripts/dspace-deploy.bat + .claude/skills/dspace-deploy/SKILL.md
  from this PR per request (local dev tooling, will live elsewhere)

* Disable SSR overlay when Cypress is driving

The overlay holds the SSR-rendered children alongside <ds-app>'s CSR-rendered
children during the masking window. Cypress's cy.get(selector) sees both
copies, so unique-id selectors return 2 elements and cy.click() fails. The
overlay is purely a UX smoothing layer (no behaviour to E2E-validate), so
short-circuit when window.Cypress is present. Browser users are unaffected.

* Advance root flicker fix to the final DOM-settle overlay mechanism (#1318, #1321)

The original overlay removal waited for ApplicationRef.isStable, which is held
hostage by post-login/admin zone activity (page looks rendered but is not
interactive - dspace-customers#725). Replace it with the final mechanism used
across the customer backports: keep the SSR snapshot until the routed <ds-app>
DOM has settled (MutationObserver + quiet window, content-height / #main-content
check, 10s cap), and make the overlay a purely visual mask so the live app stays
interactive underneath. index.html / app.component.ts / spec / typings are synced
to the final version; the existing ngAfterViewInit delay(0) is preserved.

Keeping the trunk on the final mechanism means new customer branches cut from
dtq-dev start correct instead of re-inheriting the old isStable approach.

Ref: #1318, #1321

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: jm <jm@maz>
Co-authored-by: Juraj Roka <95219754+jr-rk@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant